Skip to content
lab components / Navigation

Side navigation

Serves as a primary navigation structure within an application, providing a clear visual hierarchy to help users explore content and features.

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { SideNavigation } from "@siteimprove/fancylab";

#Examples

Ideal for applications with multiple sections and subsections, where a clear visual hierarchy is essential.

The side navigation consists of:

  • Links: Clickable text or icons leading to different content areas.
  • Icons: Visual representations of navigation items (especially important in the collapsed view).
  • Chevrons: Expandable/collapsible indicators for nested navigation levels.
  • Levels: Primary (top-level), secondary, and tertiary levels to organize content.
  • Collapsed view: Can be used to collapse the navigation in mobile or tablet views.

#Basic usage

This is a basic example, where we have selected an item under "data privacy" and the rest of the settings are at the default values.

Best practices:

  • The selected item should be visually distinct.
  • Navigation structure should mirror the content's logical hierarchy.
  • Ensure the collapsed view uses clear, recognizable icons for each top-level item.
  • Dashboard
Core Wins
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>( "dataprivacy-personal-data-types-phone-number" ); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState<string[]>(); return ( <> <div style={{ height: "100vh" }}> <SideNavigation data-observe-key="side-nav" mainItems={data.sideNavigation} bottomItems={data.sideBottomNavigation} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} searchHotkey="M" /> </div> </> );

#Falling back to default expansion

This example shows how the component falls back to showing expanded nodes that match with the selection if the expansion sent into the props are not properly showing the selected item. In this case, we select an item under "data privacy", but the expansion is set to showing something under QA. In this case, it will fall back to expanding the the nodes to show the selected item.

  • Use Case: This behavior is important when the provided expansion state doesn't align with the user's selection. It ensures the user can easily see the context of their current selection.
  • Best Practice: Clearly indicate the selected item, even when the navigation falls back to a default state. This includes a subtle animation to smoothly transition between expansion states.
  • Dashboard
Core Wins
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>( "dataprivacy-personal-data-types-phone-number" ); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState(["qa", "qa-spelling-v2"]); return ( <> <div style={{ height: "100vh" }}> <SideNavigation data-observe-key="side-nav" mainItems={data.sideNavigation} bottomItems={data.sideBottomNavigation} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} /> </div> </> );

#Using labels

Menu-items can have labels. They are rendered using the Badge component. The label can have an optional icon, text, and tooltip.

  • Use Case: To highlight new features, internal use or other contextual information.
  • Best Practice:
    • Keep labels concise and visually distinct (use the Badge component).
    • If using icons, choose universally recognizable ones.
    .
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>(null); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState<string[]>(); // Copy and wrangle the basic example to show off how to set labels const d = JSON.parse(JSON.stringify(data)); const nav: NavigationItem[] = d.sideNavigation.slice(0, 6); nav[0].labels = [{ type: "highlight1", icon: <IconFeatureFlag />, tooltip: "Feature Flag Xyz" }]; nav[1].labels = [{ type: "highlight3", text: "Internal", tooltip: "Internal use only" }]; nav[1].children![0].labels = [{ type: "highlight2", text: "Demo", tooltip: "Demo data" }]; nav[3].labels = [ { type: "warning", text: "Beta", tooltip: "Feature is still work in progress" }, { type: "highlight1", icon: <IconFeatureFlag />, tooltip: "Feature Flag Xyz" }, ]; nav[5].labels = [{ type: "highlight2", text: "Demo", tooltip: "Demo data" }]; return ( <> <div style={{ height: "35rem" }}> <SideNavigation data-observe-key="side-nav" mainItems={nav} bottomItems={[]} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} /> </div> </> );

This example has items with both href's but also onclick handlers. The idea is that simple clicks will be handled with the "onClick" handler and "preventDefault" so that the browser doesn't navigate. But if you "open in new tab" click, then the navigation happens.

  • Use Case: This pattern is useful when you need to trigger specific actions in addition to, or instead of, navigating to a new page.
  • Best Practice:
    • Clearly communicate to the user whether a click will result in navigation or a different action.
    • If the custom action fails, provide a fallback to ensure the user can still navigate to the destination.
Digital Certainty Index
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>(null); const [viewState, setViewState] = useState<ViewState>(); const [expandedItems, setExpandedItems] = useState<string[]>(); const items = [ { id: "dashboard", title: "Dashboard", icon: '\r\n\t\t\t\t\t<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\r\n\t\t\t\t\t\t<path d="M8.94204 10.6454H3.72293C2.80659 10.6454 2.04962 9.88841 2.04962 8.97207V3.75296C2.04962 2.83662 2.80659 2.07965 3.72293 2.07965H8.92212C9.85838 2.07965 10.6153 2.83662 10.6153 3.75296V8.95215C10.6153 9.88841 9.85838 10.6454 8.94204 10.6454Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M8.94204 22H3.72293C2.80659 22 2.04962 21.243 2.04962 20.3267V15.1076C2.04962 14.1912 2.80659 13.4343 3.72293 13.4343H8.92212C9.85838 13.4343 10.5954 14.1912 10.5954 15.1076V20.3068C10.6154 21.243 9.85838 22 8.94204 22Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M20.2966 13.4343H15.0775C14.1412 13.4343 13.4042 14.1912 13.4042 15.1076V20.3068C13.4042 21.243 14.1612 21.9801 15.0775 21.9801H20.2767C21.2129 21.9801 21.95 21.2231 21.95 20.3068V15.1076C21.9699 14.1912 21.2129 13.4343 20.2966 13.4343Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M20.2966 2H15.0775C14.1612 2 13.4042 2.73705 13.4042 3.67331V8.8725C13.4042 9.80876 14.1612 10.5458 15.0775 10.5458H20.2767C21.2129 10.5458 21.95 9.78884 21.95 8.8725V3.67331C21.9699 2.73705 21.2129 2 20.2966 2Z" fill="white"/>\r\n\t\t\t\t\t</svg>\r\n\t\t\t\t', observeKey: "side-nav-dashboard", children: [], onClick: () => { alert("Dashboard clicked"); }, }, { id: "dci", title: "Digital Certainty Index", icon: '\r\n\t\t\t\t\t<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">\r\n\t\t\t\t\t\t<path d="M11.7357 21.3761C6.56402 21.3761 2.35959 17.1716 2.35959 12C2.35959 6.82834 6.56402 2.6239 11.7357 2.6239C16.9073 2.6239 21.1117 6.82834 21.1117 12C21.1117 17.1716 16.9073 21.3761 11.7357 21.3761ZM11.7357 3.61086C7.11671 3.61086 3.34654 7.38103 3.34654 12C3.34654 16.6189 7.11671 20.3891 11.7357 20.3891C16.3546 20.3891 20.1248 16.6189 20.1248 12C20.1248 7.38103 16.3546 3.61086 11.7357 3.61086Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M11.7357 22.2643C7.47202 22.2643 3.58342 19.5601 2.08324 15.5333C1.82664 14.8227 2.18194 14.0134 2.89255 13.7568C3.60316 13.4804 4.41246 13.8555 4.66907 14.5661C5.77446 17.527 8.61689 19.5009 11.7357 19.5009C15.8809 19.5009 19.2365 16.1452 19.2365 12C19.2365 7.85479 15.8809 4.49913 11.7357 4.49913C10.9658 4.49913 10.3539 3.88722 10.3539 3.1174C10.3539 2.34757 10.9658 1.73566 11.7357 1.73566C17.4008 1.73566 22 6.33487 22 12C22 17.6651 17.4008 22.2643 11.7357 22.2643Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M10.3934 12C10.3934 13.0659 9.60383 13.8752 8.5774 13.8752H7.03775V10.1248H8.5774C9.60383 10.1248 10.3934 10.9341 10.3934 12ZM9.44592 12C9.44592 11.4275 9.07088 11.0525 8.55766 11.0525H8.02471V12.9475H8.55766C9.09062 12.9475 9.44592 12.5724 9.44592 12Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M10.9066 12C10.9066 10.8946 11.7357 10.0458 12.8608 10.0458C13.5517 10.0458 14.1636 10.4011 14.4794 10.934L13.6701 11.4078C13.5319 11.1314 13.2161 10.9735 12.8805 10.9735C12.2686 10.9735 11.8738 11.388 11.8738 12C11.8738 12.5921 12.2686 13.0264 12.8805 13.0264C13.2358 13.0264 13.5319 12.8685 13.6701 12.5921L14.4991 13.0659C14.1636 13.5988 13.5714 13.9541 12.8805 13.9541C11.7357 13.9541 10.9066 13.1053 10.9066 12Z" fill="white"/>\r\n\t\t\t\t\t\t<path d="M16.019 10.1248V13.8752H15.0518V10.1248H16.019Z" fill="white"/>\r\n\t\t\t\t\t</svg>\r\n\t\t\t\t', observeKey: "side-nav-dci", defaultChildId: "dci-overview", children: [ { id: "dci-overview", title: "DCI overview", href: "#dci-overview", observeKey: "side-nav-dci-overview", children: [], onClick: () => { alert("DCI overview clicked"); }, }, { id: "dci-account-overview", title: "My sites", href: "#dci-mysites", observeKey: "side-nav-dci-account-overview", children: [], onClick: async () => { console.log("DCI My sites clicked"); return fetch("https://pfg.siteimprove.com/boo").catch((e) => console.log("Response failed", e) ); }, }, ], }, ]; return ( <> <div style={{ height: "35rem" }}> <SideNavigation data-observe-key="side-nav" mainItems={items} bottomItems={[]} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} /> </div> </> );

#One main nav item and fixed view

If there is only one main nav item in mainItems the sub nav will be shown on load and the "Back to main navigation" button will be replaced by a back link that uses the props backLink and backLinkLabel. This can be combined with viewState="fixed" which will hide the view state toggle button to collapse and expand the navigation.

  • Use Case: This is perfect for applications with a single primary section and a fixed secondary navigation structure (E.g Page Inspector)
  • Best Practice: In this scenario, ensure the back link is clearly visible and labeled. Since the navigation is always visible, make sure it doesn't obscure or overlap with the main content area.
// These states are kept outside the component because you would most likely want to persist // these in something like local storage to work well will page reloads. const [selectionId, setSelectionId] = useState<string | null>(null); const [viewState, setViewState] = useState<ViewState>("fixed"); const [expandedItems, setExpandedItems] = useState<string[]>(); return ( <> <div style={{ height: "75vh" }}> <SideNavigation data-observe-key="side-nav" mainItems={dataOneMainNavItem.sideNavigation} bottomItems={[]} selectionId={selectionId} setSelectionId={setSelectionId} viewState={viewState} setViewState={setViewState} expandedItems={expandedItems} setExpandedItems={setExpandedItems} backButtonLabel="Back to Quality Assurance" backButtonUrl="http://www.siteimprove.com" /> </div> </> );

#Properties

#Guidelines

#Best practices

#General

Use SideNavigation for deep hierarchies or when users frequently switch sections.

#Placement

Place SideNavigation on the left (desktop). For smaller screen (e.g mobile), use collapsed view or Horizontal navigation.

#Style

  • Siteimprove Design System: Adhere to Siteimprove's guidelines for color, typography, and spacing. If you are not using a component from Fancy, match the styling of your SideNavigatio to existing components for visual consistency.
  • Limit navigation depth to three levels maximum.
  • Prioritize the most important navigation items at the top.
  • Match page titles to navigation link labels.

#Interaction

  • Use chevrons for expanding/collapsing nested levels.
  • Keep quaternary levels (4th level) as simple page links.
  • If more than five secondary items, consider using sub-menus.

#Responsive layout

  • Default to the expanded view.
  • Provide a collapse option with clear icons.
  • Auto-collapse for smaller screens (e.g. breakpoints of 768 pixels for tablet and mobile).

#Do not use when

  • Navigation is very simple (few top-level items).
  • Horizontal navigation is more suitable (e.g., system-level or shortcuts).

#Accessibility

#For designers

  • Ensure icon meanings are clear and labels are intuitive.

#For developers

  • Navigation link labels should be readable by assistive technologies, even when navigation is collapsed

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

  • Use clear, concise labels under 20 characters to prevent text wrapping. Labels should be scannable yet descriptive.
  • Keep labels consistent in length, style (nouns or verbs), and sentence case.